#!/usr/bin/env python
# -*- coding: UTF-8 -*-

# contact_list.py
#
#  Copyright (c) 2007 INdT (Instituto Nokia de Technologia)
#
#  Author: Kenneth Rohde Christiansen <kenneth.christiansen@gmail.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation; either version 2 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
#  USA

import pygtk
pygtk.require("2.0")
import gtk
import gobject
import pango
import tapioca
import os

(
CONTACT_LIST_COLUMN_STATUS,
CONTACT_LIST_COLUMN_INFO,
CONTACT_LIST_COLUMN_AVATAR,
CONTACT_LIST_COLUMN_ID,
CONTACT_LIST_COLUMN_PRIORITY,
) = range(5)

class ContextMenu(gtk.UIManager):
    """
    A context menu using the UIManager.

    """

    def __init__(self, parent):
        """
        A constructor for the context menu.
        """

        gtk.UIManager.__init__(self)
        self.parent = parent
        self.new()

    def new(self):
        actions = [
            ( "Block", None, "_Block...", None, None, \
                    self.on_block_unblock_menuitem_clicked),
            ( "Unblock", None, "_Unblock...", None, None, \
                    self.on_block_unblock_menuitem_clicked),
            ( "RemoveContact", None, "_Remove...", None, None,\
                    self.on_remove_contact_menuitem_clicked),
            ( "SendMessage", None, "_Send Message...", None, None,\
                    self.on_send_message_menuitem_clicked),
            ( "Call", None, "_Call...", None, None,\
                    self.on_call_menuitem_clicked),
            ( "Conference", None, "Con_ference...", None, None,\
                    self.on_conference_menuitem_clicked),
            ( "ReInvite", None, "Re-_Invite Contact...", None, None,\
                    self.on_resendinv_menuitem_clicked)
        ]
        actiongroup = gtk.ActionGroup("ContextmenuActions")
        actiongroup.add_actions(actions)
        self.insert_action_group(actiongroup, -1)

        self.add_ui_from_string( \
        '<ui>' \
        '  <popup action="popupmenu" name="popupmenu">' \
        '    <menuitem action="ReInvite" name="ReInvite"/>' \
        '    <separator name="sep0"/>' \
        '    <menuitem action="SendMessage" name="SendMessage"/>' \
        '    <menuitem action="Call" name="Call"/>' \
        '    <menuitem action="Conference" name="Conference"/>' \
        '    <separator name="sep1"/>' \
        '    <menuitem action="Block" name="Block"/>' \
        '    <menuitem action="Unblock" name="Unblock"/>' \
        '    <menuitem action="RemoveContact" name="RemoveContact"/>' \
        '  </popup>' \
        '</ui>')

        self.ensure_update()

        self.widget = self.get_widget("/popupmenu")

        self.get_widget("/popupmenu/Unblock").hide()

        self.update_menu_caps()

    def update_menu_caps(self):
        contact = self.parent.selected_contact
        presence = contact.get_presence()
        capabilities = contact.get_capabilities()

        if presence == tapioca.PRESENCE_OFFLINE:
            chat, talk, conference = False, False, False
        else:
            chat = True # Unfortunately this is needed
            talk = (capabilities & tapioca.CAPABILITY_AUDIO)
            conference = (capabilities & tapioca.CAPABILITY_VIDEO)

        self.get_widget("/popupmenu/SendMessage").set_sensitive(chat);
        self.get_widget("/popupmenu/Call").set_sensitive(talk);
        self.get_widget("/popupmenu/Conference").set_sensitive(conference);

        # Subscription status menu
        subscription_status = contact.get_subscription_status()
        if subscription_status == tapioca.SUBSCRIPTION_STATUS_REMOTE_PENDING:
            self.get_widget("/popupmenu/ReInvite").show()
        elif subscription_status == tapioca.SUBSCRIPTION_STATUS_CURRENT:
            self.get_widget("/popupmenu/sep0").hide()
            self.get_widget("/popupmenu/ReInvite").hide()

    def on_send_message_menuitem_clicked(self, widget):
        self.parent.emit('chat-session-requested', self.parent.selected_contact)

    def on_call_menuitem_clicked(self, widget):
        self.parent.emit('voip-session-requested', self.parent.selected_contact)

    def on_conference_menuitem_clicked(self, widget):
        self.parent.emit('webcam-session-requested', self.parent.selected_contact)

    def on_resendinv_menuitem_clicked(self, widget):
        self.parent.selected_contact.subscribe(True)

    def on_block_unblock_menuitem_clicked(self, widget):
        """
        This function block/unblock the selected contact.
        """
        self.parent.change_contact_block_state(self.parent.selected_contact)

    def on_remove_contact_menuitem_clicked(self, widget):
        """
        This function remove the selected contact from the user roster.
        """
        self.parent.remove_contact(self.parent.selected_contact)


class ContactListView(gobject.GObject):

    __gproperties__ = {
        'show-offline-contacts' : (gobject.TYPE_BOOLEAN, 'show offline contacts',
                    'show offline contacts in the contact list',
                    False, gobject.PARAM_READWRITE),
        'sort-after-name' : (gobject.TYPE_BOOLEAN, 'sort after name',
                    'sort depending on the name of the contact, ' \
                    'otherwise it sorts after status.',
                    False, gobject.PARAM_READWRITE),
        'show-pictures' : (gobject.TYPE_BOOLEAN, 'show pictures',
                    'show avatars in the contact list',
                    True, gobject.PARAM_READWRITE),
    }

    __gsignals__ = {
        'chat-session-requested' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    (gobject.TYPE_PYOBJECT,)),
        'voip-session-requested' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    (gobject.TYPE_PYOBJECT,)),
        'webcam-session-requested' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    (gobject.TYPE_PYOBJECT,)),
        'contact-selected' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    (gobject.TYPE_PYOBJECT,)),
        'over-contact' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
                    gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,)),
        'leave-contact' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                    []),
    }

    def __init__(self, local_contact_store):
        gobject.GObject.__init__(self)

        self.contacts_to_update = {}

        self.show_offline_contacts = False
        self.sort_after_name = False
        self.show_pictures = True
        self.selected_contact = None

        self.cursor_timeout_handler = None

        self.local_contact_store = local_contact_store
        self.local_contact_store.connect("contact-store-updated", \
                self.on_contact_store_updated)
        self.local_contact_store.connect("all-contact-store-updated", \
                self.on_all_contact_store_updated)

        path = os.path.expanduser(os.environ.get("HOME"))

        self.scrolled_win = gtk.ScrolledWindow()
        self.scrolled_win.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        self.scrolled_win.set_shadow_type(gtk.SHADOW_ETCHED_IN)

        self.contact_treeview = gtk.TreeView()
        self.contact_treeview.set_rules_hint(True)
        self.contact_model = gtk.TreeStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, str, int)
        self.contact_model.set_default_sort_func(self.compare_contacts)
        self.contact_model.set_sort_column_id(-1, gtk.SORT_ASCENDING)
        self.contact_filter = self.contact_model.filter_new()
        self.contact_filter.set_visible_func(self.contact_list_filter_func, data=None)
        self.contact_treeview.set_search_column(CONTACT_LIST_COLUMN_INFO)

        def do_contact_search(model, column, key, iterator):
            text = model.get_value(iterator, CONTACT_LIST_COLUMN_INFO)
            # The alias is between tags.
            text = text[text.find('>')+1:text.find('</')]

            if text.lower().find(key.lower()) == 0:
                return False
            return True

        self.contact_treeview.set_search_equal_func(do_contact_search)

        self.scrolled_win.add(self.contact_treeview)

        status_renderer = gtk.CellRendererPixbuf()
        avatar_renderer =  gtk.CellRendererPixbuf()
        info_renderer = gtk.CellRendererText()
        info_renderer.set_property("ellipsize", pango.ELLIPSIZE_END)
        info_renderer.set_property("ellipsize-set", True)

        contact_column = gtk.TreeViewColumn("Contacts")
        self.contact_treeview.set_headers_visible(False)
        contact_column.pack_start(status_renderer, expand=False)
        contact_column.add_attribute(status_renderer, "pixbuf", CONTACT_LIST_COLUMN_STATUS)
        contact_column.pack_start(info_renderer, expand=True)
        contact_column.add_attribute(info_renderer, "markup", CONTACT_LIST_COLUMN_INFO)
        contact_column.pack_start(avatar_renderer, expand=False)
        contact_column.add_attribute(avatar_renderer, "pixbuf", CONTACT_LIST_COLUMN_AVATAR)

        self.contact_treeview.set_model(self.contact_filter)
        self.contact_treeview.append_column(contact_column)
        self.contact_treeview.connect('row-activated', self.start_chat)
        self.contact_treeview.connect('cursor-changed', self.on_contact_selected)
        self.contact_treeview.connect('button-press-event', self.on_show_contact_context_menu)
        self.contact_treeview.connect('event', self.on_treeview_event)


    def get_widget(self):
        """
        Returns the main widget.
        """

        return self.scrolled_win


    def do_get_property(self, property):
        """
        Function that is used by the build-in gobject get_property()
        """

        if property.name == 'show-offline-contacts':
            return self.show_offline_contacts
        elif property.name == 'sort-after-name':
            return self.sort_after_name
        elif property.name == 'show-pictures':
            return self.show_pictures
        else:
            raise AttributeError, 'unknown property %s' % property.name


    def do_set_property(self, property, value):
        """
        Function that is used by the build-in gobject set_property()
        """

        if property.name == 'show-offline-contacts':
            self.show_offline_contacts = value
            self.contact_filter.refilter()
        elif property.name == 'sort-after-name':
            self.sort_after_name = value
            self.contact_model.set_default_sort_func(self.compare_contacts)
        elif property.name == 'show-pictures':
            self.show_pictures = value
            self.local_contact_store.send_all_store_updated()
        else:
            raise AttributeError, 'unknown property %s' % property.name


    def add_contacts(self, contacts):
        """
        Functions that adds the contacts to contact list object 
        by calling add_contact() for each. This function will
        connect the right signals and store available contact
        info in the local contact store.
        """

        contact_iter = iter(contacts)

        # This will hold the TreeView. Maybe there is a better place for this.
        # UPDATE: Shite! Stopped working. :(
        vadjust = self.contact_treeview.get_vadjustment()
        vadjust.clamp_page(0.0, 0.0)

        self.add_contact(contact_iter)


    def add_contact(self, contact_iter):
        """
        Function that adds one contact to the contact list object.
        It stores the contact object and the alias in the local
        contact store, as connects the signals for avatar and
        presence updates.

        If also keeps control of which contacts have emitted signals,
        as signal emission can get lost in case the signals were
        emitted before the signals were connected.

        Due to this, after proceeding all contacts the functions
        reemits signals for contacts that has their signals connected
        too late.
        """

        try:
            contact = contact_iter.next()

            # Get info
            contact_id = tapioca.ChannelTarget.get_uri(contact)
            alias = contact.get_alias()

            # Add to dictionary to make sure the contact will be updated
            self.contacts_to_update[contact.get_uri()] = contact

            print "[presence-updated] Connected for %s" % contact_id
            contact.connect("avatar-updated", self.on_contact_avatar_changed)
            contact.connect("presence-updated", self.on_contact_presence_changed)

            self.local_contact_store.set_contact(contact_id, contact)
            self.local_contact_store.set_alias(contact_id, alias)

            self.add_contact(contact_iter)

        except StopIteration, e:
            contacts_to_update = {}

            for contact_id in self.contacts_to_update:
                contacts_to_update[contact_id] = self.contacts_to_update[contact_id]

            for contact_id in contacts_to_update:
                contact = contacts_to_update[contact_id]

                contact.emit("presence-updated", 10, None)
                contact.emit("avatar-updated", None)


    def remove_contact(self, contact):
        """
        Function to remove a contact (for good) from your contact list.
        """

        if contact == None:
            return

        for row in self.contact_model:
            local_contact = self.local_contact_store.get_contact(row[CONTACT_LIST_COLUMN_ID])
            if local_contact == contact:
                it = self.contact_model.get_iter(row.path)
                self.contact_model.remove(it)

        contact_list = self.local_contact_store.get_contact_list()

        if contact_list == None:
            raise SystemError

        contact_list.remove(contact)

        return


    def change_contact_block_state(self, contact):
        if contact == None:
            return

        contact_id = contact.get_uri()
        handler_id = -1

        def do_blocked(contact, is_blocked):
            contact.disconnect(handler_id)
            self.local_contact_store.block(contact_id, is_blocked)
            self.local_contact_store.send_store_updated(contact_id)

        handler_id = contact.connect('block-status-changed', do_blocked)
        contact.block(not contact.is_blocked())


    def on_contact_avatar_changed(self, contact, new_token):
        #contact_id = tapioca.ChannelTarget.get_uri(contact)
        contact_id = contact.get_uri()

        print '----------- on_contact_avatar_changed'
        print 'new_token:     ',
        print new_token
        if new_token == None:
            new_token = contact.get_avatar_token()
        print 'new_token:    ',
        print new_token

        stored_token = self.local_contact_store.get_avatar_token(contact_id)

        print 'stored_token: ',
        print stored_token

        if stored_token != new_token:
            contact.connect("avatar-received", self.on_contact_avatar_received, new_token)
            contact.request_avatar()


    def on_contact_avatar_received(self, contact, avatar, token):
        """
        Callback function called when a new avatar is received.
        It sets the new avatar in the local contact store and 
        signals that it has been updated, so that the GUI will
        be updated as well.
        """

        contact_id = tapioca.ChannelTarget.get_uri(contact)

        print '[on_contact_avatar_received]'
        print contact_id,
        print ' - ',
        print token

        self.local_contact_store.set_avatar(contact_id, avatar, token)
        self.local_contact_store.send_store_updated(contact_id)


    def on_all_contact_store_updated(self, store, ids):
        for contact_id in ids:
            self.on_contact_store_updated(store, contact_id)


    def on_contact_store_updated(self, store, contact_id):
        alias = self.local_contact_store.get_alias(contact_id)

        if alias != None:
            alias = alias.strip("\n")
        else:
            alias = contact_id

        presence_image = self.local_contact_store.get_presence_icon(contact_id)
        presence_message = self.local_contact_store.get_presence_message(contact_id)
        custom_message = self.local_contact_store.get_custom_presence_message_only(contact_id)

        if self.show_pictures:
            avatar_thumbnail = self.local_contact_store.get_avatar_thumbnail(contact_id)
        else:
            avatar_thumbnail = None

        priority = self.local_contact_store.get_presence_priority(contact_id)

        if self.show_pictures or custom_message:
            text = "<b>" + alias + "</b>\n<small>" + presence_message + "</small>"
        else:
            text = "<b>" + alias + "</b>"

        treeiter = None

        for row in self.contact_model:
            if row[CONTACT_LIST_COLUMN_ID] == contact_id:
                treeiter = row.iter
                break

        if not treeiter:
            treeiter = self.contact_model.append(None,
                [ presence_image, text, avatar_thumbnail, contact_id, priority])
        else:
            self.contact_model.set(treeiter, CONTACT_LIST_COLUMN_STATUS, presence_image)
            self.contact_model.set(treeiter, CONTACT_LIST_COLUMN_INFO, text)
            self.contact_model.set(treeiter, CONTACT_LIST_COLUMN_AVATAR, avatar_thumbnail)
            self.contact_model.set(treeiter, CONTACT_LIST_COLUMN_PRIORITY, priority)

        path = self.contact_model.get_path(treeiter)
        self.contact_model.row_changed(path, treeiter)

        return False


    def on_contact_presence_changed(self, contact, presence, msg):
        contact_id = tapioca.ChannelTarget.get_uri(contact)

        if msg == None:
            msg = contact.get_presence_message()

        if presence == 10:
            presence = contact.get_presence()
            print "[presence-updated] forced (%s, %s)" % (contact_id, \
                self.local_contact_store.get_presence_message_from_presence(presence))
        else:
            print "[presence-updated] caught! (%s)" % contact_id

        try:
            del self.contacts_to_update[contact_id]
        except KeyError, e:
            pass

        self.local_contact_store.set_presence(contact_id, presence)
        self.local_contact_store.set_presence_message(contact_id, msg)
        self.local_contact_store.send_store_updated(contact_id)


    def on_show_contact_context_menu(self, widget, event):
        if event.button == 3:
            pthinfo = widget.get_path_at_pos(int(event.x), int(event.y))

            if pthinfo:
                path, col, cellx, celly = pthinfo
                widget.grab_focus()
                widget.set_cursor( path, col, 0)
                contact_id = self.contact_model.get_value ( \
                        self.contact_model.get_iter (path), CONTACT_LIST_COLUMN_ID)
                self.selected_contact = self.local_contact_store.get_contact(contact_id)

                if self.selected_contact:
                    menu = ContextMenu(self)
                    # TODO: Change Block/Unblock label based on contact block state

                    menu.new()
                    if self.selected_contact.is_blocked():
                        menu.get_widget("/popupmenu/Block").hide()
                        menu.get_widget("/popupmenu/Unblock").show()
                    else:
                        menu.get_widget("/popupmenu/Unblock").hide()
                        menu.get_widget("/popupmenu/Block").show()

                    menu.widget.popup(None, None, None, event.button, event.time)

            return True


    def on_treeview_event(self, widget, event):
        if event.type == gtk.gdk.MOTION_NOTIFY:
            tmp = self.contact_treeview.get_path_at_pos(int(event.x), int(event.y))

            if tmp != None:
                over_path = tmp[0]
                iter = self.contact_model.get_iter(over_path)

                if iter != None:
                    contact_id = self.contact_model.get_value(iter, CONTACT_LIST_COLUMN_ID)

                    column = self.contact_treeview.get_column(0)
                    cell = self.contact_treeview.get_cell_area(over_path, column)

                    gdk_window = self.contact_treeview.get_bin_window()
                    tree_left, tree_top = gdk_window.get_origin()
                    geo = gdk_window.get_geometry()
                    tree_width = geo[2]

                    cell_left_screen_coord = tree_left
                    cell_right_screen_coord = tree_left + tree_width
                    cell_top_screen_coord = tree_top + cell.y

                    self.emit('over-contact', contact_id, cell_right_screen_coord, \
                            cell_left_screen_coord, cell_top_screen_coord)

        elif event.type == gtk.gdk.ENTER_NOTIFY:
            pass

        elif event.type == gtk.gdk.LEAVE_NOTIFY:
            self.emit('leave-contact')

        return False


    def on_contact_selected(self, treeview):
        self.emit('contact-selected', self.get_selected())


    def compare_contacts(self, treemodel, iter1, iter2, user_data=None):
        """
        Function to compare contacts. Used when sorting the 
        contact list view.
        """

        priority1, alias1 = treemodel.get(iter1, CONTACT_LIST_COLUMN_PRIORITY, \
                CONTACT_LIST_COLUMN_INFO)
        priority2, alias2 = treemodel.get(iter2, CONTACT_LIST_COLUMN_PRIORITY, \
                CONTACT_LIST_COLUMN_INFO)

        if self.sort_after_name:
            # reset priority, just not offline (4), 
            # we still show last
            if priority1 < 5:
                priority1 = 1
            if priority2 < 5:
                priority2 = 1

        if not alias2:
            return -1

        contact1 = str(priority1) + alias1.lower()
        contact2 = str(priority2) + alias2.lower()

        if contact1 < contact2:
            return -1
        elif contact1 > contact2:
            return 1
        else:
            return 0


    def contact_list_filter_func(self, model, iter, user_data):
        if self.show_offline_contacts:
            return True
        return model.get_value(iter, CONTACT_LIST_COLUMN_PRIORITY) != 5


    def start_chat(self, treeview, path, view_column):
        contact_id = self.contact_model.get_value (self.contact_model.get_iter (path), \
                CONTACT_LIST_COLUMN_ID)
        contact = self.local_contact_store.get_contact(contact_id)
        self.emit('chat-session-requested', contact)


    def get_selected(self):
        """
        Get the id of the selected contact.
        """

        selection = self.contact_treeview.get_selection()
        model, model_iter = selection.get_selected()

        if model_iter:
            contact_id = model[model_iter][CONTACT_LIST_COLUMN_ID]
        else:
            contact_id = None

        return contact_id


    def show(self):
        """
        Show the contact list view.
        """

        self.get_widget().show()


    def hide(self):
        """
        Hide the contact list view.
        """

        self.get_widget().hide()


    def clear(self):
        """
        Clear the contact list view of contacts.
        """

        self.contact_model.clear()


gobject.type_register(ContactListView)
